/****************************************************************************
 * Copyright (c) 2008, 2009 Andreas Unger and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Andreas Unger - initial API and implementation 
 ****************************************************************************/

package org.eclipselabs.damos.execution.datatype;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipselabs.damos.dml.BooleanDataType;
import org.eclipselabs.damos.dml.Component;
import org.eclipselabs.damos.dml.DMLFactory;
import org.eclipselabs.damos.dml.DataKind;
import org.eclipselabs.damos.dml.DataType;
import org.eclipselabs.damos.dml.ExplicitOutputDataTypeSpecification;
import org.eclipselabs.damos.dml.OutputPort;
import org.eclipselabs.damos.execution.executionmodel.DataFlowSourceEnd;
import org.eclipselabs.damos.execution.executionmodel.DataFlowTargetEnd;
import org.eclipselabs.damos.execution.executionmodel.ExecutionModel;
import org.eclipselabs.damos.execution.executionmodel.Node;

/**
 * @author Andreas Unger
 *
 */
public class DataTypeResolver {

	public void resolve(ExecutionModel model, DataType defaultDataType) throws InvalidDataTypeException {
		new Context(model, defaultDataType).execute();
	}

	private static class Context {
		
		private ExecutionModel model;
		private DataType defaultDataType;
		
		/**
		 * 
		 */
		public Context(ExecutionModel model, DataType defaultDataType) {
			this.model = model;
			this.defaultDataType = defaultDataType;
		}
		
		public void execute() throws InvalidDataTypeException {
			List<Node> backlog = new LinkedList<Node>(model.getNodes());

			for (Iterator<Node> it = backlog.iterator(); it.hasNext();) {
				Node node = it.next();
				Component component = node.getComponent();
				if (isPureLogicComponent(component)) {
					node.setDataType(DMLFactory.eINSTANCE.createBooleanDataType());
					it.remove();
				} else if (component.getOutputDataTypeSpecification() instanceof ExplicitOutputDataTypeSpecification) {
					DataType dataType = ((ExplicitOutputDataTypeSpecification) component.getOutputDataTypeSpecification()).getDataType();
					if (dataType != null) {
						node.setDataType((DataType) EcoreUtil.copy(dataType));
						it.remove();
					}
				}
			}
			
			setComponentDataTypeToInputDataType(backlog, false);
			setComponentDataTypeToInputDataType(backlog, true);
			setComponentDataTypeToTargetInputDataType(backlog);

			for (Node node : backlog) {
				node.setDataType((DataType) EcoreUtil.copy(defaultDataType));
			}
			
			List<Node> invalidNodes = new ArrayList<Node>();
			
			for (Node node : model.getNodes()) {
				if (!validateDataTypes(node)) {
					invalidNodes.add(node);
				}
			}

			if (!invalidNodes.isEmpty()) {
				throw new InvalidDataTypeException(invalidNodes);
			}
		}
		
		private boolean isPureLogicComponent(Component component) {
			List<OutputPort> outputPorts = component.getOutputPorts();
			if (outputPorts.isEmpty()) {
				return false;
			}
			for (OutputPort outputPort : outputPorts) {
				if (outputPort.getOutput().getDataKind() != DataKind.LOGIC) {
					return false;
				}
			}
			return true;
		}
		
		private void setComponentDataTypeToInputDataType(List<Node> backlog, boolean considerBooleanDataType) {
			boolean changed;
			do {
				changed = false;
				for (Iterator<Node> it = backlog.iterator(); it.hasNext();) {
					Node node = it.next();
					DataType dataType = getInputDataType(node, considerBooleanDataType);
					if (dataType != null) {
						node.setDataType((DataType) EcoreUtil.copy(dataType));
						it.remove();
						changed = true;
					}
				}
			} while (changed);
		}
				
		private void setComponentDataTypeToTargetInputDataType(List<Node> backlog) {
			boolean changed;
			do {
				changed = false;
				for (Iterator<Node> it = backlog.iterator(); it.hasNext();) {
					Node node = it.next();
					DataType dataType = getTargetInputDataType(node); 
					if (dataType != null) {
						node.setDataType((DataType) EcoreUtil.copy(dataType));
						it.remove();
						changed = true;
					}
				}
			} while (changed);
		}
		
		private DataType getTargetInputDataType(Node node) {
			DataType targetComponentDataType = null;
			for (DataFlowSourceEnd sourceEnd : node.getOutgoingDataFlows()) {
				for (DataFlowTargetEnd targetEnd : sourceEnd.getDataFlow().getTargetEnds()) {
					Node targetNode = targetEnd.getNode(); 
					DataType dataType = getInputDataType(targetNode, false);
					if (dataType == null) {
						dataType = getInputDataType(targetNode, true);
					}
					if (dataType != null) {
						if (targetComponentDataType != null && !EcoreUtil.equals(dataType, targetComponentDataType)) {
							return null;
						}
						targetComponentDataType = dataType;
					}
				}
			}
			return targetComponentDataType;
		}
		
		private DataType getInputDataType(Node node, boolean considerBooleanDataType) {
			for (DataFlowTargetEnd targetEnd : node.getIncomingDataFlows()) {
				DataType dataType = getIncomingDataFlowDataType(targetEnd);
				if (dataType != null && (considerBooleanDataType || !(dataType instanceof BooleanDataType))) {
					return dataType;
				}
			}
			return null;
		}
		
		private boolean validateDataTypes(Node node) {
			for (DataFlowTargetEnd targetEnd : node.getIncomingDataFlows()) {
				if (targetEnd.getPort().getInput().getDataKind() == DataKind.SCALAR) {
					if (getIncomingDataFlowDataType(targetEnd) instanceof BooleanDataType) {
						return false;
					}
				} else if (targetEnd.getPort().getInput().getDataKind() == DataKind.LOGIC) {
					if (!(getIncomingDataFlowDataType(targetEnd) instanceof BooleanDataType)) {
						return false;
					}
				}
			}
			
			for (DataFlowSourceEnd sourceEnd : node.getOutgoingDataFlows()) {
				if (sourceEnd.getPort().getOutput().getDataKind() == DataKind.SCALAR) {
					if (node.getDataType() instanceof BooleanDataType) {
						return false;
					}
				} else if (sourceEnd.getPort().getOutput().getDataKind() == DataKind.LOGIC) {
					if (!(node.getDataType() instanceof BooleanDataType)) {
						return false;
					}
				}
			}
			
			DataType dataType = null;
			for (DataFlowTargetEnd targetEnd : node.getIncomingDataFlows()) {
				if (targetEnd.getPort().getInput().getDataKind() != DataKind.LOGIC) {
					DataType t = getIncomingDataFlowDataType(targetEnd);
					if (t == null) {
						return false;
					}
					if (dataType == null) {
						dataType = t;
					} else if (!EcoreUtil.equals(dataType, t)) {
						return false;
					}
				}
			}
			
			return true;
		}
		
		private DataType getIncomingDataFlowDataType(DataFlowTargetEnd targetEnd) {
			return targetEnd.getDataFlow().getSourceEnd().getNode().getDataType();
		}

	}
	
}
